/**
 * Title :	jFamilyTree generator
 * Description: jFamilyTree generator
 * Version:	provider
 * Copyright (c) 2001-2008
 * Terms of use:see license agreement at http://treebolic.sourceforge.net/en/license.htm
 * Author: Bernard Bou
 * Company: bsys
 */
package jFamilyTree.generator.model;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;

import jFamilyTree.generator.Pair;
import jFamilyTree.model.IEdge;
import jFamilyTree.model.INode;
import jFamilyTree.model.MenuItem;
import jFamilyTree.model.Model;
import jFamilyTree.model.MountPoint;
import jFamilyTree.model.MutableNode;
import jFamilyTree.model.Settings;
import jFamilyTree.model.Tree;
import jFamilyTree.model.TreeMutableEdge;
import jFamilyTree.model.TreeMutableNode;
import jFamilyTree.model.Utils;

/**
 * Model utilities
 * 
 * @author Bernard Bou
 */
public class ModelUtils
{
	// T O . M U T A B L E

	/**
	 * Make of copy of this model with mutable nodes
	 * 
	 * @param thisModel
	 *        model
	 * @return equivalent model with mutable nodes
	 */
	static public Pair<Model, Map<String, MutableNode>> toMutable(Model thisModel)
	{
		Map<String, MutableNode> thisIdToNodeMap = new TreeMap<String, MutableNode>();
		Map<INode, MutableNode> thisOldToNewNodeMap = new HashMap<INode, MutableNode>();

		// nodes
		INode thisRoot = toMutable(thisModel.theTree.getRoot(), thisIdToNodeMap, thisOldToNewNodeMap);

		// edges
		List<IEdge> theseEdges = null;
		List<IEdge> thoseEdges = thisModel.theTree.getEdges();
		if (theseEdges != null)
		{
			theseEdges = new ArrayList<IEdge>();
			for (IEdge thatEdge : thoseEdges)
			{
				TreeMutableEdge thisEdge = new TreeMutableEdge(thatEdge);
				thisEdge.setFrom(thisOldToNewNodeMap.get(thatEdge.getFrom()));
				thisEdge.setTo(thisOldToNewNodeMap.get(thatEdge.getTo()));
				theseEdges.add(thisEdge);
			}
		}
		return new Pair<Model, Map<String, MutableNode>>(new Model(new Tree(thisRoot, theseEdges), thisModel.theSettings), thisIdToNodeMap);
	}

	/**
	 * Make of copy of this model with mutable nodes
	 * 
	 * @param thatNode
	 *        start node
	 * @param thisIdToNodeMap
	 *        id to node map
	 * @param thisOldToNewNodeMap
	 *        old node to new node map
	 * @return equivalent model with mutable nodes
	 */
	static public TreeMutableNode toMutable(INode thatNode, Map<String, MutableNode> thisIdToNodeMap, Map<INode, MutableNode> thisOldToNewNodeMap)
	{
		// this node
		TreeMutableNode thisNode = new TreeMutableNode(thatNode);
		thisIdToNodeMap.put(thisNode.getId(), thisNode);
		thisOldToNewNodeMap.put(thatNode, thisNode);

		// children
		List<INode> thoseChildren = thatNode.getChildren();
		if (thoseChildren != null)
		{
			List<INode> theseChildren = new ArrayList<INode>();
			for (INode thatChildNode : thoseChildren)
			{
				TreeMutableNode thisChild = toMutable(thatChildNode, thisIdToNodeMap, thisOldToNewNodeMap);

				// tree link
				thisChild.setParent(thisNode);
				theseChildren.add(thisChild);
			}
			thisNode.setChildren(theseChildren);
		}

		return thisNode;
	}

	// M A K E . I D . M A P

	/**
	 * Make id to node map for this model
	 * 
	 * @param thisModel
	 *        model
	 * @return id to node map
	 */
	static public Map<String, MutableNode> makeIdToNodeMap(Model thisModel)
	{
		Map<String, MutableNode> thisIdToNodeMap = new TreeMap<String, MutableNode>();
		makeIdToNodeMap((MutableNode) thisModel.theTree.getRoot(), thisIdToNodeMap);
		return thisIdToNodeMap;
	}

	/**
	 * Make id to node map for this model
	 * 
	 * @param thisNode
	 *        start node
	 * @param thisIdToNodeMap
	 *        id to node result map
	 */
	static private void makeIdToNodeMap(MutableNode thisNode, Map<String, MutableNode> thisIdToNodeMap)
	{
		// this node
		thisIdToNodeMap.put(thisNode.getId(), thisNode);

		// recurse
		List<INode> theseChildren = thisNode.getChildren();
		if (theseChildren != null)
		{
			for (INode thisChildNode : theseChildren)
				makeIdToNodeMap((MutableNode) thisChildNode, thisIdToNodeMap);
		}
	}

	// N O R M A L I Z E . I D S

	/**
	 * Normalize ids in this model
	 * 
	 * @param thisModel
	 *        model
	 * @param thisPrefix
	 *        id prefix
	 * @return new id to node map
	 */
	static public Map<String, MutableNode> normalizeIds(Model thisModel, String thisPrefix)
	{
		Map<String, MutableNode> thisIdToNodeMap = new TreeMap<String, MutableNode>();
		Map<String, String> thisOldIdToNewIdMap = new HashMap<String, String>();
		normalizeId((TreeMutableNode) thisModel.theTree.getRoot(), thisPrefix, thisIdToNodeMap, thisOldIdToNewIdMap);
		normalizeLinks((TreeMutableNode) thisModel.theTree.getRoot(), thisOldIdToNewIdMap);

		return thisIdToNodeMap;
	}

	/**
	 * Normalize ids in model
	 * 
	 * @param thisNode
	 *        node
	 * @param thisPrefix
	 *        id prefix
	 * @param thisIdToNodeMap
	 *        id to node result map
	 * @param thisOldIdToNewIdMap
	 *        old id to new id result map
	 */
	static private void normalizeId(TreeMutableNode thisNode, String thisPrefix, Map<String, MutableNode> thisIdToNodeMap, Map<String, String> thisOldIdToNewIdMap)
	{
		// this node
		String thisOldId = thisNode.getId();
		String thisId = thisNode.getParent() == null ? "root" : thisPrefix; //$NON-NLS-1$
		thisNode.setId(thisId);

		// record in maps
		thisIdToNodeMap.put(thisId, thisNode);
		thisOldIdToNewIdMap.put(thisOldId, thisId);

		// recurse
		List<INode> theseChildren = thisNode.getChildren();
		if (theseChildren != null)
		{
			int i = 0;
			for (INode thisChildNode : theseChildren)
				normalizeId((TreeMutableNode) thisChildNode, thisPrefix + "-" + i++, thisIdToNodeMap, thisOldIdToNewIdMap); //$NON-NLS-1$
		}
	}

	/**
	 * Normalize node links
	 * 
	 * @param thisNode
	 *        start node
	 * @param thisOldIdToNewIdMap
	 *        old id to new id map
	 */
	static private void normalizeLinks(TreeMutableNode thisNode, Map<String, String> thisOldIdToNewIdMap)
	{
		// this node
		String thisLink = thisNode.getLink();
		if (thisLink != null && !thisLink.isEmpty())
		{
			thisLink = decode(thisLink);
			int thisPos = thisLink.indexOf('#');
			if (thisPos != -1 && thisLink.length() > thisPos + 1)
			{
				String thisOldId = thisLink.substring(thisPos + 1);
				String thisNewId = thisOldIdToNewIdMap.get(thisOldId);
				if (thisNewId != null)
				{
					thisLink = thisLink.substring(0, thisPos) + thisNewId;
					thisNode.setLink('#' + encode(thisLink));
				}
			}
		}

		// recurse
		List<INode> theseChildren = thisNode.getChildren();
		if (theseChildren != null)
		{
			for (INode thisChildNode : theseChildren)
				normalizeLinks((TreeMutableNode) thisChildNode, thisOldIdToNewIdMap);
		}
	}

	// I M A G E . M A P

	/**
	 * Map of images in this model to their locations
	 * 
	 * @param thisModel
	 *        model
	 * @return map of images to locations
	 */
	static public Map<String, SortedSet<String>> getImageMap(Model thisModel)
	{
		Map<String, SortedSet<String>> thisMap = new TreeMap<String, SortedSet<String>>();

		String thisImageFile;

		// defaults
		if (thisModel.theSettings != null)
		{
			thisImageFile = thisModel.theSettings.theBackgroundImage;
			if (thisImageFile != null && !thisImageFile.isEmpty())
				putReferenceMapKeyValue(thisMap, thisImageFile, "background"); //$NON-NLS-1$

			thisImageFile = thisModel.theSettings.theDefaultNodeImage;
			if (thisImageFile != null && !thisImageFile.isEmpty())
				putReferenceMapKeyValue(thisMap, thisImageFile, "default node"); //$NON-NLS-1$

			thisImageFile = thisModel.theSettings.theDefaultTreeEdgeImage;
			if (thisImageFile != null && !thisImageFile.isEmpty())
				putReferenceMapKeyValue(thisMap, thisImageFile, "default tree-edge"); //$NON-NLS-1$

			thisImageFile = thisModel.theSettings.theDefaultEdgeImage;
			if (thisImageFile != null && !thisImageFile.isEmpty())
				putReferenceMapKeyValue(thisMap, thisImageFile, "default edge"); //$NON-NLS-1$
		}

		// nodes
		getImageMap(thisModel.theTree.getRoot(), thisMap);

		// edges
		List<IEdge> theseEdges = thisModel.theTree.getEdges();
		if (theseEdges != null)
			for (IEdge thisEdge : theseEdges)
			{
				thisImageFile = thisEdge.getImageFile();
				if (thisImageFile != null && !thisImageFile.isEmpty())
					putReferenceMapKeyValue(thisMap, thisImageFile, "edge:" + thisEdge.getFrom().getId() + "->" + thisEdge.getTo().getId()); //$NON-NLS-1$ //$NON-NLS-2$
			}
		return thisMap;
	}

	/**
	 * Recursive traversal of nodes for images
	 * 
	 * @param thisNode
	 *        start node
	 * @param thisMap
	 *        map to collect results
	 */
	static private void getImageMap(INode thisNode, Map<String, SortedSet<String>> thisMap)
	{
		// this node
		String thisImageFile = thisNode.getImageFile();
		if (thisImageFile != null && !thisImageFile.isEmpty())
			putReferenceMapKeyValue(thisMap, thisImageFile, "node:" + thisNode.getId()); //$NON-NLS-1$
		thisImageFile = thisNode.getEdgeImageFile();
		if (thisImageFile != null && !thisImageFile.isEmpty())
			putReferenceMapKeyValue(thisMap, thisImageFile, "treeedge:" + thisNode.getId()); //$NON-NLS-1$

		// recurse
		List<INode> theseChildren = thisNode.getChildren();
		if (theseChildren != null)
			for (INode thisChild : theseChildren)
				getImageMap(thisChild, thisMap);
	}

	// M O U N T . M A P

	/**
	 * Map of mounts in this model to their locations
	 * 
	 * @param thisModel
	 *        model
	 * @return map of mounts to locations
	 */
	static public Map<String, SortedSet<String>> getMountMap(Model thisModel)
	{
		Map<String, SortedSet<String>> thisMap = new TreeMap<String, SortedSet<String>>();
		getMountMap(thisModel.theTree.getRoot(), thisMap);
		return thisMap;
	}

	/**
	 * Recursive traversal of nodes for mounts
	 * 
	 * @param thisNode
	 *        start node
	 * @param thisMap
	 *        map to collect results
	 */
	static private void getMountMap(INode thisNode, Map<String, SortedSet<String>> thisMap)
	{
		// this node
		MountPoint thisMountPoint = thisNode.getMountPoint();
		if (thisMountPoint != null && thisMountPoint instanceof MountPoint.Mounting)
		{
			MountPoint.Mounting thisMountingPoint = (MountPoint.Mounting) thisMountPoint;
			putReferenceMapKeyValue(thisMap, thisMountingPoint.theURL, "node:" + thisNode.getId()); //$NON-NLS-1$
		}

		// recurse
		List<INode> theseChildren = thisNode.getChildren();
		if (theseChildren != null)
			for (INode thisChild : theseChildren)
				getMountMap(thisChild, thisMap);
	}

	// M O U N T . M A P

	/**
	 * Map of mounts in this model to their locations
	 * 
	 * @param thisModel
	 *        model
	 * @return map of mounts to locations
	 */
	static public Map<String, SortedSet<String>> getLinkMap(Model thisModel)
	{
		Map<String, SortedSet<String>> thisMap = new TreeMap<String, SortedSet<String>>();
		getLinkMap(thisModel.theTree.getRoot(), thisMap);
		return thisMap;
	}

	/**
	 * Recursive traversal of nodes for mounts
	 * 
	 * @param thisNode
	 *        start node
	 * @param thisMap
	 *        map to collect results
	 */
	static private void getLinkMap(INode thisNode, Map<String, SortedSet<String>> thisMap)
	{
		// this node
		String thisLink = thisNode.getLink();
		if (thisLink != null && !thisLink.isEmpty())
			putReferenceMapKeyValue(thisMap, thisLink, "node:" + thisNode.getId()); //$NON-NLS-1$

		// recurse
		List<INode> theseChildren = thisNode.getChildren();
		if (theseChildren != null)
			for (INode thisChild : theseChildren)
				getLinkMap(thisChild, thisMap);
	}

	// I D . M A P

	/**
	 * Map of ids in this model to their locations
	 * 
	 * @param thisModel
	 *        model
	 * @return map of ids to locations
	 */
	static public Map<String, SortedSet<String>> getIdMap(Model thisModel)
	{
		Map<String, SortedSet<String>> thisMap = new TreeMap<String, SortedSet<String>>();
		getIdMap(thisModel.theTree.getRoot(), thisMap);
		return thisMap;
	}

	/**
	 * Recursive traversal of nodes for ids
	 * 
	 * @param thisNode
	 *        start node
	 * @param thisMap
	 *        map to collect results
	 */
	static private void getIdMap(INode thisNode, Map<String, SortedSet<String>> thisMap)
	{
		// this node
		String thisId = thisNode.getId();
		if (thisId == null || thisId.isEmpty())
			thisId = "null"; //$NON-NLS-1$
		INode thisParent = thisNode.getParent();
		putReferenceMapKeyValue(thisMap, thisId, "label:" + thisNode.getLabel() + " parent:" + (thisParent == null ? "null" : thisParent.getId())); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$

		// recurse
		List<INode> theseChildren = thisNode.getChildren();
		if (theseChildren != null)
			for (INode thisChild : theseChildren)
				getIdMap(thisChild, thisMap);
	}

	// R E F E R E N C E . H E L P E R

	/**
	 * Put value in multi-valued map
	 * 
	 * @param thisMap
	 *        map
	 * @param thisKey
	 *        key
	 * @param thisValue
	 *        value
	 */
	static private void putReferenceMapKeyValue(Map<String, SortedSet<String>> thisMap, String thisKey, String thisValue)
	{
		SortedSet<String> theseLocations = thisMap.get(thisKey);
		if (theseLocations == null)
		{
			theseLocations = new TreeSet<String>();
			thisMap.put(thisKey, theseLocations);
		}
		theseLocations.add(thisValue);
	}

	// D E C O D E / E N C O D E

	/**
	 * Decode encoded URL (for display)
	 * 
	 * @param thisString
	 *        encode URL string
	 * @return decoded URL string
	 */
	static private String decode(String thisString)
	{
		try
		{
			return URLDecoder.decode(thisString, "UTF8"); //$NON-NLS-1$
		}
		catch (UnsupportedEncodingException e)
		{
			System.err.println("Can't decode " + thisString + " - " + e); //$NON-NLS-1$ //$NON-NLS-2$
		}
		return thisString;
	}

	/**
	 * Encode encoded URL
	 * 
	 * @param thisString
	 *        encode URL string
	 * @return decoded URL string
	 */
	static private String encode(String thisString)
	{
		try
		{
			return URLEncoder.encode(thisString, "UTF8"); //$NON-NLS-1$
		}
		catch (UnsupportedEncodingException e)
		{
			System.err.println("Can't decode " + thisString + " - " + e); //$NON-NLS-1$ //$NON-NLS-2$
		}
		return thisString;
	}

	/**
	 * Save settings to file
	 * 
	 * @param thisPropertyFile
	 *        property file to save to
	 */
	static public void saveSettings(Settings theseSettings, String thisPropertyFile)
	{
		Properties theseProperties = settingsToProperty(theseSettings);
		try
		{
			theseProperties.store(new FileOutputStream(thisPropertyFile), "TREEBOLIC-SETTINGS");
		}
		catch (IOException e)
		{
			e.printStackTrace();
		}
	}

	/**
	 * Save settings
	 * 
	 * @param theseSettings
	 *        settings to convert to properties
	 */
	static public Properties settingsToProperty(Settings theseSettings)
	{
		Properties theseProperties = new Properties();

		// top
		if (theseSettings.theHasToolbarFlag != null)
			theseProperties.setProperty("toolbar", theseSettings.theHasToolbarFlag.toString());
		if (theseSettings.theHasStatusbarFlag != null)
			theseProperties.setProperty("statusbar", theseSettings.theHasStatusbarFlag.toString());
		if (theseSettings.theHasPopUpMenuFlag != null)
			theseProperties.setProperty("popupmenu", theseSettings.theHasPopUpMenuFlag.toString());
		if (theseSettings.theHasToolTipFlag != null)
			theseProperties.setProperty("tooltip", theseSettings.theHasToolTipFlag.toString());
		if (theseSettings.theToolTipDisplaysContentFlag != null)
			theseProperties.setProperty("tooltip-displays-content", theseSettings.theToolTipDisplaysContentFlag.toString());
		if (theseSettings.theFocus != null)
			theseProperties.setProperty("focus", theseSettings.theFocus);
		if (theseSettings.theFocusOnHoverFlag != null)
			theseProperties.setProperty("focus-on-hover", theseSettings.theFocusOnHoverFlag.toString());
		if (theseSettings.theXMoveTo != null)
			theseProperties.setProperty("xmoveto", theseSettings.theXMoveTo.toString());
		if (theseSettings.theYMoveTo != null)
			theseProperties.setProperty("ymoveto", theseSettings.theYMoveTo.toString());
		if (theseSettings.theXShift != null)
			theseProperties.setProperty("xshift", theseSettings.theXShift.toString());
		if (theseSettings.theYShift != null)
			theseProperties.setProperty("yshift", theseSettings.theYShift.toString());

		// tree
		if (theseSettings.theOrientation != null)
			theseProperties.setProperty("orientation", theseSettings.theOrientation.toString());
		if (theseSettings.theExpansion != null)
			theseProperties.setProperty("expansion", theseSettings.theExpansion.toString());
		if (theseSettings.theSweep != null)
			theseProperties.setProperty("sweep", theseSettings.theSweep.toString());
		if (theseSettings.thePreserveOrientationFlag != null)
			theseProperties.setProperty("preserve-orientation", theseSettings.thePreserveOrientationFlag.toString());
		if (theseSettings.theFontFace != null)
			theseProperties.setProperty("fontface", theseSettings.theFontFace);
		if (theseSettings.theFontSize != null)
			theseProperties.setProperty("fontsize", theseSettings.theFontSize.toString());
		if (theseSettings.theFontSizeStep != null)
			theseProperties.setProperty("fontsizestep", theseSettings.theFontSizeStep.toString());
		if (theseSettings.theBackColor != null)
			theseProperties.setProperty("backcolor", Utils.colorToString(theseSettings.theBackColor));
		if (theseSettings.theForeColor != null)
			theseProperties.setProperty("forecolor", Utils.colorToString(theseSettings.theForeColor));
		if (theseSettings.theBackgroundImage != null)
			theseProperties.setProperty("background.image", theseSettings.theBackgroundImage);

		// nodes
		if (theseSettings.theNodeBackColor != null)
			theseProperties.setProperty("node.backcolor", Utils.colorToString(theseSettings.theNodeBackColor));
		if (theseSettings.theNodeForeColor != null)
			theseProperties.setProperty("node.forecolor", Utils.colorToString(theseSettings.theNodeForeColor));
		if (theseSettings.theDefaultNodeImage != null)
			theseProperties.setProperty("node.image", theseSettings.theDefaultNodeImage.toString());

		// edges
		if (theseSettings.theHyperbolicEdgesFlag != null)
			theseProperties.setProperty("edge.hyperbolic", theseSettings.theHyperbolicEdgesFlag.toString());
		if (theseSettings.theDefaultEdgeImage != null)
			theseProperties.setProperty("edge.image", theseSettings.theDefaultEdgeImage);
		if (theseSettings.theDefaultTreeEdgeImage != null)
			theseProperties.setProperty("tree.edge.image", theseSettings.theDefaultTreeEdgeImage);
		if (theseSettings.theEdgeColor != null)
			theseProperties.setProperty("edge.color", Utils.colorToString(theseSettings.theEdgeColor));
		if (theseSettings.theTreeEdgeColor != null)
			theseProperties.setProperty("tree.edge.color", Utils.colorToString(theseSettings.theTreeEdgeColor));
		if (theseSettings.theEdgeStyle != null)
		{
			String thisParam;
			thisParam = Utils.toString(theseSettings.theEdgeStyle, Utils.StyleComponent.STROKE);
			if (thisParam != null)
				theseProperties.setProperty("edge.stroke", thisParam);
			thisParam = Utils.toString(theseSettings.theEdgeStyle, Utils.StyleComponent.FROMTERMINATOR);
			if (thisParam != null)
				theseProperties.setProperty("edge.fromterminator", thisParam);
			thisParam = Utils.toString(theseSettings.theEdgeStyle, Utils.StyleComponent.TOTERMINATOR);
			if (thisParam != null)
				theseProperties.setProperty("edge.toterminator", thisParam);
			thisParam = Utils.toString(theseSettings.theEdgeStyle, Utils.StyleComponent.HIDDEN);
			if (thisParam != null)
				theseProperties.setProperty("edge.hidden", thisParam);
		}
		if (theseSettings.theTreeEdgeStyle != null)
		{
			String thisParam;
			thisParam = Utils.toString(theseSettings.theTreeEdgeStyle, Utils.StyleComponent.STROKE);
			if (thisParam != null)
				theseProperties.setProperty("tree.edge.stroke", thisParam);
			thisParam = Utils.toString(theseSettings.theEdgeStyle, Utils.StyleComponent.FROMTERMINATOR);
			if (thisParam != null)
				theseProperties.setProperty("tree.edge.fromterminator", thisParam);
			thisParam = Utils.toString(theseSettings.theEdgeStyle, Utils.StyleComponent.TOTERMINATOR);
			if (thisParam != null)
				theseProperties.setProperty("tree.edge.toterminator", thisParam);
			thisParam = Utils.toString(theseSettings.theEdgeStyle, Utils.StyleComponent.HIDDEN);
			if (thisParam != null)
				theseProperties.setProperty("tree.edge.hidden", thisParam);
		}

		// menu
		int i = 0;
		final char thisSep = ';';
		for (MenuItem thisMenuItem : theseSettings.theMenu)
		{
			StringBuffer thisParam = new StringBuffer();
			thisParam.append(thisMenuItem.theLabel);
			thisParam.append(thisSep);
			if (thisMenuItem.theAction != null)
				thisParam.append(thisMenuItem.theAction.toString().toLowerCase());
			thisParam.append(thisSep);
			if (thisMenuItem.theLink != null)
				thisParam.append(thisMenuItem.theLink.toString().toLowerCase());
			thisParam.append(thisSep);
			if (thisMenuItem.theTarget != null)
				thisParam.append(thisMenuItem.theTarget.toString().toLowerCase());
			thisParam.append(thisSep);
			if (thisMenuItem.theMatchTarget != null)
				thisParam.append(thisMenuItem.theMatchTarget.toString().toLowerCase());
			thisParam.append(thisSep);
			if (thisMenuItem.theMatchScope != null)
				thisParam.append(thisMenuItem.theMatchScope.toString().toLowerCase());
			thisParam.append(thisSep);
			if (thisMenuItem.theMatchMode != null)
				thisParam.append(thisMenuItem.theMatchMode.toString().toLowerCase());
			theseProperties.setProperty("menuitem" + i++, thisParam.toString());
		}
		return theseProperties;
	}
}
